#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>

#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/sched.h>
#include <linux/kthread.h>

#define DEVICE_NAME					"cpuload"

#define CPULOAD_IOC_MAGIC			'C'
#define CPULOAD_IOC_MAXNR			2
#define CPULOAD_GETVALUE			_IOR(CPULOAD_IOC_MAGIC, 1, unsigned long long)
#define CPULOAD_SETTIME				_IOW(CPULOAD_IOC_MAGIC, 2, unsigned int)

struct cpuload_info
{
	unsigned long long user;
	unsigned long long nice;
	unsigned long long system;
	unsigned long long idle;
	unsigned long long iowait;
	unsigned long long irq;
	unsigned long long softirq;
};

static struct task_struct *work_thread_handle = NULL;

static DEFINE_SEMAPHORE(global_value_lock);
static DEFINE_SEMAPHORE(global_time_lock);

static unsigned int refer_time = 0;
static unsigned long long idle_time = 0.0;
static unsigned long long total_time = 0.0;

static int get_offset_next(unsigned char *buffer)
{
	unsigned char *offset = buffer;

	while(' ' != *offset)
	{
		if(0 == *offset)
		{
			return 0;
		}
		offset++;
	}

	while(' ' == *offset)
	{
		if(0 == *offset)
		{
			return 0;
		}
		offset++;
	}

	if(buffer < offset)
	{
		return offset - buffer;
	}
	else
	{
		return 0;
	}
}

static int parser_stat(unsigned char *buffer, struct cpuload_info *info)
{
	if(NULL != buffer && NULL != info)
	{
		buffer += get_offset_next(buffer);
		info->user = simple_strtoull((const char *)buffer, (char **)0, 0);
		buffer += get_offset_next(buffer);
		info->nice = simple_strtoull((const char *)buffer, (char **)0, 0);
		buffer += get_offset_next(buffer);
		info->system = simple_strtoull((const char *)buffer, (char **)0, 0);
		buffer += get_offset_next(buffer);
		info->idle = simple_strtoull((const char *)buffer, (char **)0, 0);
		buffer += get_offset_next(buffer);
		info->iowait = simple_strtoull((const char *)buffer, (char **)0, 0);
		buffer += get_offset_next(buffer);
		info->irq = simple_strtoull((const char *)buffer, (char **)0, 0);
		buffer += get_offset_next(buffer);
		info->softirq = simple_strtoull((const char *)buffer, (char **)0, 0);
		return 0;
	}
	return -1;
}

static int cpuload_work_thread(void *arg)
{
	ssize_t ret = 0;
	
	unsigned int delay_time = 0;

	struct file *pfile = NULL;
	unsigned char buffer[64] = {0};
	loff_t pos = 0;

	struct cpuload_info info = {0, 0, 0, 0, 0, 0, 0};
	
	unsigned long long cpuload_idle_before = 0;
	unsigned long long cpuload_total_before = 0;
	unsigned long long cpuload_idle_current = 0;
	unsigned long long cpuload_total_current = 0;

	do
	{
		msleep(1000);

		pfile = filp_open("/proc/stat", O_RDONLY, 0);
	} while(!kthread_should_stop() && NULL == pfile);
	
	if(!kthread_should_stop() && NULL != pfile)
	{
		do
		{
			down(&global_time_lock);
			delay_time = refer_time;
			up(&global_time_lock);

			msleep(delay_time);
			
			if(NULL != pfile)
			{
				pos = 0;
				ret = vfs_read(pfile, buffer, sizeof(buffer), &pos);

				if(64 <= ret)
				{
					if('c' == buffer[0] && 'p' == buffer[1] && 'u' == buffer[2] && ' ' == buffer[3] && ' ' == buffer[4])
					{
						cpuload_idle_before = info.idle;
						cpuload_total_before = info.user + info.nice + info.system + info.idle + info.iowait + info.irq + info.softirq;

						parser_stat(buffer, &info);

						if(0 < cpuload_idle_before && 0 < cpuload_total_before && cpuload_idle_before < cpuload_total_before)
						{
							cpuload_idle_current = info.idle;
							cpuload_total_current = info.user + info.nice + info.system + info.idle + info.iowait + info.irq + info.softirq;
							
							//printk(KERN_INFO "%llu:%llu, %llu:%llu\n", cpuload_idle_before, cpuload_total_before, cpuload_idle_current, cpuload_total_current);
							
							if(cpuload_idle_current < cpuload_total_current && cpuload_idle_before <= cpuload_idle_current && cpuload_total_before <= cpuload_total_current)
							{
								down(&global_value_lock);
								idle_time = cpuload_idle_current - cpuload_idle_before;
								total_time = cpuload_total_current - cpuload_total_before;
								up(&global_value_lock);
							}
						}
					}
				}
			}
		} while(!kthread_should_stop() && NULL != pfile);
	}
	
	if(NULL != pfile)
	{
		filp_close(pfile, NULL);
		pfile = NULL;
	}
	
	return 0;
}

static long cpuload_ioctl(struct file *flip, unsigned int cmd, unsigned long arg)
{
	ssize_t ret = 0;
	
	unsigned int delay_time = 0;

	if ((_IOC_TYPE(cmd) != CPULOAD_IOC_MAGIC) || (_IOC_NR(cmd) > CPULOAD_IOC_MAXNR))
 	{
		return -EINVAL;
	}

	if(NULL == work_thread_handle)
	{
		work_thread_handle = kthread_create(cpuload_work_thread, NULL, "cpuload");
		if(NULL == work_thread_handle)
		{
			return -ECHILD;
		}
		wake_up_process(work_thread_handle);
	}

	switch(cmd)
	{
		case CPULOAD_GETVALUE:
			if(0 < arg)
			{
				down(&global_value_lock);
				put_user(idle_time, (unsigned long long *)arg);
				put_user(total_time, ((unsigned long long *)arg) + 1);
				up(&global_value_lock);
				ret = 0;
			}
			else
			{
				ret = -EINVAL;
			}
			break;
		case CPULOAD_SETTIME:
			get_user(delay_time, (unsigned int *)arg);
			if(2000 > delay_time && 10 < delay_time)
			{
				down(&global_time_lock);
				refer_time = delay_time;
				up(&global_time_lock);
				ret = 0;
			}
			else
			{
				ret = -EINVAL;
			}
			break;
		default:
			ret = -EINVAL;
			break;
	}

	return ret;
}

static struct file_operations cpuload_fops = {
	.owner			=	THIS_MODULE,
	.unlocked_ioctl	=	cpuload_ioctl,
};

static struct miscdevice misc = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = DEVICE_NAME,
	.fops = &cpuload_fops,
};

static int __init cpuload_init(void)
{
	if(0 == misc_register(&misc))
	{
		work_thread_handle = kthread_create(cpuload_work_thread, NULL, "cpuload");
		if(NULL != work_thread_handle)
		{
			down(&global_time_lock);
			refer_time = 1000;
			up(&global_time_lock);
			wake_up_process(work_thread_handle);
			return 0;
		}
	}

	return -ENOMEM;
}

static void __exit cpuload_exit(void)
{
	if(NULL != work_thread_handle)
	{
		kthread_stop(work_thread_handle);
		work_thread_handle = NULL;
	}
	misc_deregister(&misc);
}

module_init(cpuload_init);
module_exit(cpuload_exit);

MODULE_AUTHOR("Yan Jun");
MODULE_DESCRIPTION("calc cpu load");
MODULE_VERSION("0.1");
MODULE_LICENSE("GPL");
MODULE_ALIAS("cpuload");
